module canvas.sdlcanvas;

import derelict.sdl.sdl;
import utils.vector2;
import utils.color;
import utils.time;
import utils.perfcount;
import canvas.abstractcanvas;
import std.stdio;
import std.string;

///event handler delegate
///return true if you handled the event
alias bool delegate(SDL_Event event) SDLEventConsumer;
///called every time FPS value is updated
alias void delegate(float curFPS) FPSUpdate;

class SDLCanvas: AbstractCanvas {
    private bool mFullScreen;
    private bool mDoubleBuf;

    private bool mTerminated = false;
    private bool mRendering = false;

    private SDL_Surface* mScreen;
    private int mVideoFlags;

    private SDLEventConsumer[] mEvConsumers;

    private static Time cFPSTimeSpan; //how often to recalc FPS
    private float mFPSLastValue;

    private Vector2i mNewRes;

    ///set this delegate to be notified every time FPS value changes
    public FPSUpdate onFPSUpdate;

    this(Vector2i res, bool fullScreen, bool doubleBuf) {
        super(res);
        mNewRes = res;

        cFPSTimeSpan = timeSecs(1);

        mFullScreen = fullScreen;
        mDoubleBuf = doubleBuf;
        mEvConsumers = new SDLEventConsumer[0];

        //initialize sdl
        if (SDL_Init(SDL_INIT_VIDEO) < 0) {
            throw new Exception("SDL init failed");
        }
        scope(failure) SDL_Quit();

        //determine video capabilities
        SDL_VideoInfo* vidInf;
        vidInf = SDL_GetVideoInfo();
        if (!vidInf)
        {
            throw new Exception("SDL_GetVideoInfo() failed");
        }

        mVideoFlags = SDL_HWPALETTE | SDL_RESIZABLE;  // Store the palette in hardware
        if (mDoubleBuf) mVideoFlags |= SDL_DOUBLEBUF;    // Enable double buffering
        if (mFullScreen) mVideoFlags |= SDL_FULLSCREEN;       // Enable fullscreen
//        if (vidInf.blit_hw) mVideoFlags |= SDL_HWACCEL; // This checks if hardware blits can be done

        //create screen surface
        mScreen = SDL_SetVideoMode(mRes.x, mRes.y, 32, mVideoFlags);
        if (!mScreen) {
            throw new Exception("SDL_SetVideoMode() failed");
        }

        SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);

        initPerf();
        setCurrentTimeDelegate(&getCurrentTime);
    }

    ~this() {
        SDL_Quit();
    }

    private Time getCurrentTime() {
        //int ticks = SDL_GetTicks();
        //return timeMsecs(ticks);
        long perfc = queryPerfMicros();
        return timeMusecs(perfc);
    }

    ///add a delegate that will receive events
    public void addEventConsumer(SDLEventConsumer consumer) {
        mEvConsumers.length = mEvConsumers.length + 1;
        mEvConsumers[mEvConsumers.length - 1] = consumer;
    }

    ///event handler, call to handle all pending events and forward to
    ///registered consumer delegates
    public void processEvents() {
        bool handled = false;
        SDL_Event ev;

        while (SDL_PollEvent(&ev)) {
            foreach (SDLEventConsumer consumer; mEvConsumers) {
                handled = consumer(ev);
                if (handled)
                    return;
            }

            switch (ev.type) {
                case SDL_QUIT:
                    terminate();
                    break;
                case SDL_KEYDOWN:
                    if (ev.key.keysym.sym == SDLK_ESCAPE)
                        terminate();
                    break;
                case SDL_VIDEORESIZE:
                    mNewRes = Vector2i(ev.resize.w, ev.resize.h);
                    checkSize();
                    break;
                default:
                    break;
            }
        }
    }

    public void enterRenderMode() {
        mRendering = true;

        //lock surface, if necessary
        if (SDL_MUSTLOCK(mScreen))
            while(SDL_LockSurface(mScreen) < 0) {};
    }

    public void leaveRenderMode() {
        //unlock surface, if locked
        if (SDL_MUSTLOCK(mScreen))
            SDL_UnlockSurface(mScreen);

        if (mDoubleBuf)
            SDL_Flip(mScreen);

        //FPS calculation
        static Time lastTime;
        static uint frameCount;
        static Time lastcurtime;

        Time curtime = getCurrentTime();
        mDeltaT = curtime - lastcurtime;
        lastcurtime = curtime;
        if (curtime >= lastTime + cFPSTimeSpan) {
            mFPSLastValue = (cast(float)frameCount
                / (curtime - lastTime).msecs) * 1000.0f;
            if (onFPSUpdate)
                onFPSUpdate(mFPSLastValue);
            lastTime = curtime;
            frameCount = 0;
        }
        frameCount++;

        mRendering = false;
        //process any pending window resize
        checkSize();
    }

    ///set text in window titlebar
    public void setWindowTitle(char[] title) {
        SDL_WM_SetCaption(std.string.toStringz(title),null);
    }

    public void drawPixel(Vector2i pos, RGBColor col) {
        static uint color;

        //color = SDL_MapRGB(mScreen.format, col.rByte, col.gByte, col.bByte);
        //color = col.rByte << 16 | col.gByte << 8 | col.bByte;
        color = cast(ubyte)(255.0f*col.r) << 16 | cast(ubyte)(255.0f*col.g) << 8 | cast(ubyte)(255.0f*col.b);

        *(cast(uint *)mScreen.pixels + pos.y * (mScreen.pitch >> 2) + pos.x) = color;
    }

    public void getPixelColor(Vector2i pos, out RGBColor col) {
        ubyte red, green, blue, alpha;
        SDL_PixelFormat *fmt;
        int bpp;
        Uint32 temp, pixel;

        fmt=mScreen.format;

        // Lock the m_pScreen
        SDL_LockSurface(mScreen);

        bpp = mScreen.format.BytesPerPixel;

        if (bpp != 4) writefln(stderr, "\nWARNING: 32 bits/pixel assumed!");

        // Here p is the address to the pixel we want to retrieve
        ubyte *p = cast(ubyte *)mScreen.pixels + pos.y * mScreen.pitch + pos.x * bpp;
        pixel = *cast(uint *)p;
        SDL_UnlockSurface(mScreen);

        /* Get Red component */
        temp=pixel&fmt.Rmask; /* Isolate red component */
        temp=temp>>fmt.Rshift;/* Shift it down to 8-bit */
        temp=temp<<fmt.Rloss; /* Expand to a full 8-bit number */
        red=cast(ubyte)temp;

        /* Get Green component */
        temp=pixel&fmt.Gmask; /* Isolate green component */
        temp=temp>>fmt.Gshift;/* Shift it down to 8-bit */
        temp=temp<<fmt.Gloss; /* Expand to a full 8-bit number */
        green=cast(ubyte)temp;

        /* Get Blue component */
        temp=pixel&fmt.Bmask; /* Isolate blue component */
        temp=temp>>fmt.Bshift;/* Shift it down to 8-bit */
        temp=temp<<fmt.Bloss; /* Expand to a full 8-bit number */
        blue=cast(ubyte)temp;

        /* Get Alpha component */
        temp=pixel&fmt.Amask; /* Isolate alpha component */
        temp=temp>>fmt.Ashift;/* Shift it down to 8-bit */
        temp=temp<<fmt.Aloss; /* Expand to a full 8-bit number */
        alpha=cast(ubyte)temp;

        col = RGBColor(red/255.0f, green/255.0f, blue/255.0f);
    }

    ///fast clear of canvas, using SDL_FillRect
    public void clearCanvas(RGBColor col) {
        static uint color;

        color = SDL_MapRGB(mScreen.format, col.rByte, col.gByte, col.bByte);

        SDL_FillRect(mScreen, null, color);
    }

    public void fillRect(Vector2i p1, Vector2i p2, RGBColor col) {
        static uint color;
        static SDL_Rect r;
        r.x = p1.x;
        r.y = p1.y;
        r.w = p2.x - p1.x;
        r.h = p2.y - p1.y;

        color = SDL_MapRGB(mScreen.format, col.rByte, col.gByte, col.bByte);

        SDL_FillRect(mScreen, &r, color);
    }

    public void copyLine(int from, int to) {
        static SDL_Rect r, r2;
        r.x = 0;
        r.y = from;
        r.w = mScreen.w;
        //very dirty hack to work around some blit oddity/AV
        if (to == mScreen.h-1)
            r.w--;
        r.h = 1;
        r2.x = 0;
        r2.y = to;
        SDL_BlitSurface(mScreen, &r, mScreen, &r2);
    }

    ///prepare canvas for resize, actual resize is done when rendering finishes
    public void resizeCanvas(Vector2i newRes) {
        //cannot resize while rendering, so store new size and just try
        //the resize here
        mNewRes = newRes;
        checkSize();
    }

    ///check if canvas should be resized, and perform resize if not rendering
    private void checkSize() {
        if (!mRendering && (mNewRes.x != mRes.x || mNewRes.y != mRes.y)) {
            writefln("Resizing SDL canvas to %d x %d",mNewRes.x,mNewRes.y);
            mRes = mNewRes;
            mScreen = SDL_SetVideoMode(mRes.x, mRes.y, 32, mVideoFlags);
            if (!mScreen) {
                throw new Exception("SDL_SetVideoMode() failed");
            }
        }
    }

    public void captureMouse(bool capture) {
        if (capture) {
            SDL_ShowCursor(0);
            SDL_WM_GrabInput(SDL_GRAB_ON);
        } else {
            SDL_ShowCursor(1);
            SDL_WM_GrabInput(SDL_GRAB_OFF);
        }
    }

    public SDL_Surface* screen() {
        return mScreen;
    }

    public bool terminated() {
        return mTerminated;
    }

    public void terminate() {
        mTerminated = true;
    }

    public bool rendering() {
        return mRendering;
    }
}
